﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Numerics;
using SafeCalculations.SafeTypes;
using System.Threading;

namespace SafeCalculations
{
    /// <summary>
    /// Provides methods for calculating statistics that are immune to numeric overflow/underflow
    /// </summary>
    public static class SafeStatistics
    {
        /// <summary>
        /// Calculates the average by creating the exact value as a fraction and then converting that to a double. No numeric overflow/underflow is posible as long as no more than 2*32 values are used (in which case <paramref name="values"/>.Count would be inaccurate anyway, so this is unlikely in practice). 
        /// </summary>
        /// <param name="sum">The numerator of the average (total sum of all values being averaged).</param>
        /// <param name="totalCount">The denominator of the average (the total number of values being averaged).</param>
        /// <returns></returns>
        internal static double CalculateAverage(Int32WithBoundedRollover sum, uint totalCount)
        {
            if (sum.FitsIntoInt32)
                return (1.0 * sum.ToInt32()) / totalCount;
            else
            {
                BigInteger remainder;
                BigInteger divisionResult = BigInteger.DivRem(sum.ToBigInteger(), totalCount, out remainder);

                int integerValue = (int)divisionResult;
                double decimalValue = ((int)remainder) / (1.0 * totalCount);
                return (integerValue + decimalValue);
            }
        }

        /// <summary>
        /// Calculates an exact average (no rounding).
        /// </summary>
        /// <param name="sum">The numerator of the average (total sum of all values being averaged).</param>
        /// <param name="totalCount">The denominator of the average (the total number of values being averaged).</param>
        /// <returns></returns>
        private static ExactAverage CalculateExactAverage(BigInteger sum, BigInteger totalCount)
        {
            BigInteger remainder;
            BigInteger divisionResult = BigInteger.DivRem(sum, totalCount, out remainder);

            return new ExactAverage((int)divisionResult, remainder, totalCount);
        }

        /// <summary>
        /// *Suboptimal* Ideally this method should not be used since accessing entries through a <see cref="IEnumerator"/> adds a lot of overhead (also it is meant to be used for large collections of values). This method is included so that there can be instant gratification (you can just run this on any large collection and it will just work). Ideally this method should not be used if performance is a concern though. An <see cref="ExactAverage"/> is returned instead of a <see cref="double"/> so that a 100% accurate result is returned.
        /// </summary>
        /// <param name="values"></param>
        /// <param name="bufferSize"></param>
        /// <returns></returns>
        public static ExactAverage CalculateAverage(IEnumerable<int> values, uint bufferSize = 8000)
        {
            //For a large number of values more than one Int32WithBoundedRollover may be needed.
            List<Int32WithBoundedRollover> totals = new List<Int32WithBoundedRollover>();

            Int32WithBoundedRollover currentTotalCounter = new Int32WithBoundedRollover();
            uint numValuesCountedForCurrentTotalCounter = 0;

            //Use a temporary buffer to store the values read from the enumerator. This helps to trim down on function call overhead (also Int32WithBoundedRollover by design only allows adding an array of values for this reason).
            int[] buffer = new int[bufferSize];

            IEnumerator<int> valuesEnumerator = values.GetEnumerator();
            while (true)
            {
                uint numConsumed = ConsumeNextN(valuesEnumerator, (uint)buffer.Length, ref buffer);
                if (numConsumed == 0)
                    break;

                //When adding in the next block of values some of the values may be added to the current total counter as well as the next total counter if they won't all fit in the current one.
                uint remainingSpace = uint.MaxValue - numValuesCountedForCurrentTotalCounter;
                if (remainingSpace >= numConsumed)
                    currentTotalCounter.Add(buffer, 0, numConsumed);
                else
                {
                    currentTotalCounter.Add(buffer, 0, remainingSpace);

                    totals.Add(currentTotalCounter);
                    currentTotalCounter = new Int32WithBoundedRollover();

                    currentTotalCounter.Add(buffer, remainingSpace, numConsumed - remainingSpace);
                }

                //By allowing the counter for the number of values to rollover it remains accurate when a new total counter is added.
                unchecked { numValuesCountedForCurrentTotalCounter += numConsumed; }
            }

            totals.Add(currentTotalCounter);
            BigInteger sum = totals.Select(t => t.ToBigInteger()).Aggregate<BigInteger>((v1, v2) => v1 + v2);

            //The number of values used is the maximum (uint.MaxValue) for a total counter for all total counters except the last one plus the current number of values counted for the current total counter.
            BigInteger numValues = numValuesCountedForCurrentTotalCounter;
            for (int i = 0; i < totals.Count - 1; i++)
                numValues += uint.MaxValue;

            return CalculateExactAverage(sum, numValues);
        }

        /// <summary>
        /// Reads the next <paramref name="numToConsume"/> values from <paramref name="valuesEnumerator"/> and places them into <paramref name="buffer"/> starting at index 0.
        /// </summary>
        /// <param name="valuesEnumerator"></param>
        /// <param name="numToConsume"></param>
        /// <param name="buffer"></param>
        /// <returns></returns>
        private static uint ConsumeNextN(IEnumerator<int> valuesEnumerator, uint numToConsume, ref int[] buffer)
        {
            uint numConsumed = 0;
            while (numConsumed < numToConsume)
            {
                //End the loop prematurely if the enumerator is out of values
                if (!valuesEnumerator.MoveNext())
                    break;

                buffer[numConsumed++] = valuesEnumerator.Current;
            }

            //Return the number of consumed values which may be less than numToConsume if the enumerator doesn't have enough values left
            return numConsumed;
        }
    }
}
